package executives.thread_managers;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;

import utils.StateCamera;

import components.AbstractNeuron;

/**
 * The spike producer class takes an array indicating indices and spike times
 * of input neurons as well as an array of input and reservoir neurons, and
 * organizes them into tasks to be run. 
 * @author zach
 *
 */
public class SpikeProducer implements Runnable {

	/** The default number of neurons contained in each update task. */
	private static final int DEFAULT_CHUNK_SIZE = 100;
	
	/** The number of neurons contained in each update task. */
	private static int chunkSize = DEFAULT_CHUNK_SIZE;
	
	/** 
	 * A count-down latch to prevent unintentional non-linear behavior.
	 * Specifically, used to prevent the program from progressing past
	 * the simulation before it is complete.
	 */
	private static CountDownLatch latch;
	
	public static CountDownLatch syncLatch;

	
	/** The camera recording the states of each neuron which reports. */
	private StateCamera stateCamera;
	
	//TODO: Feels redundant... case for an improved layer class or overhaul of state camera?
	/** Whether or not the camera is on for these neurons */
	private boolean cameraOn;
	
	/** Where this producer stores the tasks it produces. */
	private final BlockingQueue<Runnable> queue;
	
	/** 
	 * An array of non-input neuron (autonomous) where the neurons are
	 * grouped such that each row represents a "chunk" of neurons that
	 * will make up a single task.
	 */
	private AbstractNeuron [][] groupedNeurons;
	
	//TODO: Comes up way too many times... start working on making Network functional so it can keep track of global variables like this.
	/** The current time in the simulation. */
	private double time = 0.0;
	
	private final double endTime;
	
	/** The time-step of integration of the neurons in the tasks. */
	private double timeStep = 1.0;
	
	//TODO: Change this so that the network can keep running in the absence of input for some designated amount of time
	/** 
	 * A signal if there are any more tasks which can be added to the queue
	 * based on the spike train.
	 */
	private boolean moreIns;
	
	private int numConsumers;
	

	/**
	 * 
	 * @param queue
	 * @param timeStep
	 * @param endTime
	 * @param l
	 * @param stateCamera
	 * @param neurons
	 * @param numConsumers
	 */
	public SpikeProducer(BlockingQueue<Runnable> queue, double timeStep,
			double endTime, CountDownLatch l, StateCamera stateCamera,
			AbstractNeuron [] neurons, int numConsumers){
		this.numConsumers = numConsumers;
		this.queue = queue;
		this.timeStep = timeStep;
		latch = l;
		this.stateCamera = stateCamera;
		this.endTime = endTime;
		cameraOn = stateCamera != null;
		moreIns = endTime > time;
		if(cameraOn)
			stateCamera.setParallel(true);
		
		int synapses = 0;
		for(int i = 0, n = neurons.length; i < n; i++){
			synapses += neurons[i].getNumSynapses();
		}
		
		int synsPerTask = (int) Math.floor((double)(synapses/(numConsumers * 3.0)));
		int runningSyns = 0;
		TreeMap<Integer, ArrayList<AbstractNeuron>> nGroups =
				new TreeMap<Integer, ArrayList<AbstractNeuron>>();
		ArrayList<AbstractNeuron> group = new ArrayList<AbstractNeuron>();
		
		for(int i = 0, n = neurons.length; i < n; i++){
			
			runningSyns += neurons[i].getNumSynapses();
			group.add(neurons[i]);
			
			if(runningSyns >= synsPerTask) {
				nGroups.put(runningSyns, group);	
				group = new ArrayList<AbstractNeuron>();
				runningSyns = 0;
			}
			
			if(i == n - 1 && group.size() != 0) {
				for(AbstractNeuron neuron : group) {
					Entry<Integer, ArrayList<AbstractNeuron>> ent =
							nGroups.pollFirstEntry();
					int newKey = ent.getKey() + neuron.getNumSynapses();
					ArrayList<AbstractNeuron> newVal = ent.getValue();
					newVal.add(neuron);
					nGroups.put(newKey, newVal);								
				}
				
			}
			
		}
		
		groupedNeurons = new AbstractNeuron[nGroups.size()][];
		int ind = 0;
		for(Integer i : nGroups.keySet()) {
			group = nGroups.get(i);
			AbstractNeuron[] g = new AbstractNeuron[group.size()];
			for(int j = 0, n = group.size(); j < n; j++){
				g[j] = group.get(j);
			}
			groupedNeurons[ind] = g;
			ind++;
		}
		
	}

	/**
	 * 
	 * @param queue
	 * @param timeStep
	 * @param time
	 * @param endTime
	 * @param latch
	 * @param stateCamera
	 * @param neurons
	 * @param numConsumers
	 */
	public SpikeProducer(BlockingQueue<Runnable> queue,
			double timeStep, double time, double endTime,
			CountDownLatch latch, StateCamera stateCamera,
			AbstractNeuron[] neurons, int numConsumers){	
		
		this(queue, timeStep, endTime, latch, stateCamera, neurons,
				numConsumers);
		setTime(time);	
		
	}
	
	/**
	 * 
	 */
	@Override
	public void run() {
		try{
			
			moreIns = endTime >= time;
			if(!moreIns){
				return;
			}
			
			double percent = 0.1;
			double runningPercent;
			double t = time - time;
			double et = endTime - time;
						
			do {	
				
				if(queue.isEmpty()) {						
					
					produce();	
					t = time;
					syncLatch.await();
					if(time >= endTime){
						moreIns = false;
						return;
					}
					
					if(cameraOn && moreIns){
						syncLatch = new CountDownLatch(1);
						moreIns = !stateCamera.collectFrame();
						syncLatch.await();
					}
					
					runningPercent = (t / et);	
					if(runningPercent >= percent) {
						percent += 0.1;
						System.out.print((int)(runningPercent * 100) + "% ");
					}
					
				}		
				queue.size();
			} while(moreIns);
		
		} catch (Exception e) {
			e.printStackTrace();
		}
		
		System.out.println();
		latch.countDown();		
		
	}
	
	/**
	 * 
	 */
	private void produce(){
		
		try {
			
			if(!queue.isEmpty()) {
				throw new InterruptedException("Illegal Queue State: Tasks" +
						" cannot be added to a non-empty queue");
			}
			
			LinkedList<Runnable> preList = new LinkedList<Runnable>();
			for(AbstractNeuron [] group : groupedNeurons) {
				preList.add(new NeuronUpdater(group, timeStep));
			}
			
			syncLatch = new CountDownLatch(preList.size());
			
			queue.addAll(preList);
				
			time = time + timeStep;	
								
		}catch (InterruptedException ie){
			System.err.println(ie.getMessage());
			ie.printStackTrace();
			System.exit(1);
		} catch (IllegalStateException ise){
			System.err.println(ise.getMessage());
			ise.printStackTrace();
			System.exit(1);
		}
		
	}
	
	/*/********************************************************************
	 * 							GETTERS AND SETTERS						  *
	 **********************************************************************/
	
	public static int getChunkSize() {
		return chunkSize;
	}

	public static void setChunkSize(int cs) {
		chunkSize = cs;
	}

	public double getTime() {
		return time;
	}
	
	public void setTime(double t){
		time = t;
	}
	
	public boolean isExhausted(){
		return !moreIns;
	}
	
	/*/********************************************************************
	 * 				Private Classes (Run Reservoir & Run Inputs)          *
	 **********************************************************************/
	
	/**
	 * 
	 * @author zach
	 *
	 */
	private static class NeuronUpdater implements Runnable {
		
		private AbstractNeuron [] neurons;
		private double timeStep;
		
		public NeuronUpdater (AbstractNeuron [] neurons, double timeStep) {
			this.neurons = neurons;
			this.timeStep = timeStep;
		}
		
		public void run () {
			for (int i = 0, n = neurons.length; i < n; i++) {
				neurons[i].update(timeStep);
			}
		}
		
	}

	public BlockingQueue<Runnable> getQueue() {
		return queue;
	}

	public synchronized CountDownLatch getSyncLatch() {
		return syncLatch;
	}

	public int getNumConsumers() {
		return numConsumers;
	}

	public void setNumConsumers(int numConsumers) {
		this.numConsumers = numConsumers;
	}

}
